# frozen_string_literal: true

module EE
  module Gitlab
    module BackgroundMigration
      # https://gitlab.com/gitlab-org/gitlab/-/issues/384222
      # Part of :deprecate_vulnerability_feedbacks involves adjusting the attachment
      # of issues to vulnerability findings to use the Vulnerability::IssueLink and
      # Vulnerability::MergeRequestLink models instead of relating the with
      # Vulnerability::Feedback. Existing vulnerabilities need to be attached in this fashion.
      module CreateVulnerabilityLinks
        extend ActiveSupport::Concern
        extend ::Gitlab::Utils::Override
        FEEDBACK_TYPE_ISSUE = 1
        FEEDBACK_TYPE_MERGE_REQUEST = 2

        prepended do
          scope_to ->(relation) do
            relation.where(feedback_type: [FEEDBACK_TYPE_ISSUE, FEEDBACK_TYPE_MERGE_REQUEST])
          end
          feature_category :vulnerability_management
          operation_name :create_vulnerability_links
        end

        # rubocop:disable Style/Documentation
        class Project < ApplicationRecord
          self.table_name = "projects"
        end

        class User < ApplicationRecord
          self.table_name = "users"
        end

        class Feedback < ::ApplicationRecord
          include EachBatch
          self.table_name = "vulnerability_feedback"

          belongs_to :project, class_name: 'Project'
          belongs_to :author, class_name: 'User'
          belongs_to :issue, class_name: 'Issue'
          belongs_to :merge_request, class_name: 'MergeRequest'
          belongs_to :finding,
            primary_key: :uuid,
            foreign_key: :finding_uuid,
            class_name: 'Finding',
            inverse_of: :feedbacks

          belongs_to :security_finding,
            primary_key: :uuid,
            foreign_key: :finding_uuid,
            class_name: 'SecurityFinding',
            inverse_of: :feedbacks

          def self.match_on_finding_uuid_or_security_finding_or_project_fingerprint
            where('EXISTS (SELECT 1 FROM vulnerability_occurrences WHERE vulnerability_occurrences.uuid =
                vulnerability_feedback.finding_uuid::varchar)')
              .or(where('EXISTS (SELECT 1 FROM vulnerability_occurrences WHERE
              vulnerability_occurrences.project_fingerprint = vulnerability_feedback.project_fingerprint::bytea)'))
              .or(where('EXISTS (SELECT 1 FROM security_findings WHERE security_findings.uuid =
              vulnerability_feedback.finding_uuid)'))
          end
        end

        class Finding < ::ApplicationRecord
          include ShaAttribute

          validates :details, json_schema: { filename: "vulnerability_finding_details" }, if: false

          sha_attribute :project_fingerprint
          sha_attribute :location_fingerprint

          self.table_name = "vulnerability_occurrences"

          belongs_to :vulnerability, class_name: 'Vulnerability'
          has_many :feedbacks, class_name: 'Feedback', inverse_of: :finding, primary_key: 'uuid',
            foreign_key: 'finding_uuid'
        end

        class SecurityFinding < ::ApplicationRecord
          include PartitionedTable

          self.table_name = 'security_findings'
          self.primary_key = :id
          self.ignored_columns = [:partition_number]

          partitioned_by :partition_number,
            strategy: :sliding_list,
            next_partition_if: ->(_) { false },
            detach_partition_if: ->(_) { false }

          has_many :feedbacks,
            class_name: 'Feedback',
            inverse_of: :security_finding,
            primary_key: 'uuid',
            foreign_key: 'finding_uuid'

          validates :finding_data, json_schema: { filename: "filename" }, if: false
        end

        class Vulnerability < ApplicationRecord
          self.table_name = "vulnerabilities"

          has_many :findings, class_name: '::Finding', inverse_of: :vulnerability
        end

        class IssueLink < ApplicationRecord
          self.table_name = "vulnerability_issue_links"

          belongs_to :issue, inverse_of: :issue_links
          belongs_to :vulnerability

          enum link_type: { related: 1, created: 2 } # 'related' is the default value

          validates :vulnerability, :issue, presence: true
          validates :issue_id,
            uniqueness: { scope: :vulnerability_id, message: N_('has already been linked to another vulnerability') }
          validates :vulnerability_id,
            uniqueness: {
              conditions: -> { where(link_type: 'created') },
              message: N_('already has a "created" issue link')
            },
            if: :created?
        end

        class MergeRequestLink < ApplicationRecord
          self.table_name = "vulnerability_merge_request_links"

          belongs_to :merge_request, inverse_of: :merge_request_links
          belongs_to :vulnerability

          validates :vulnerability, :merge_request, presence: true
          validates :merge_request_id,
            uniqueness: { scope: :vulnerability_id, message: N_('is already linked to this vulnerability') }
        end

        class Issue < ApplicationRecord
          self.table_name = "issues"

          has_many :issue_links, inverse_of: :issue
        end

        class MergeRequest < ApplicationRecord
          self.table_name = "merge_requests"

          has_many :merge_request_links, inverse_of: :merge_request
        end
        # rubocop:enable Style/Documentation

        override :perform

        def perform
          each_sub_batch do |sub_batch|
            feedbacks = Feedback.where(id: sub_batch.pluck(:id))

            with_vulnerabilities_finding = drop_invalid_records(
              feedbacks.match_on_finding_uuid_or_security_finding_or_project_fingerprint
                        .select { |feedback| !feedback.finding.nil? }
            )

            without_vulnerability, with_vulnerability = with_vulnerabilities_finding.partition do |feedback|
              feedback.finding.vulnerability_id.nil?
            end

            security_finding_only = drop_invalid_records(feedbacks).select do |feedback|
              !feedback.security_finding.nil? && feedback.finding.nil?
            end

            handle_vulnerability_present_scenario(with_vulnerability)
            handle_vulnerability_finding_present_scenario(without_vulnerability)
            handle_security_findings_only_scenario(security_finding_only)
          end
        end

        private

        # Filter off vulnerbility feedback issues with no associated issue.
        # Merge request filtering like this is precautionary, but none exist.
        def drop_invalid_records(feedbacks)
          feedbacks.reject do |feedback|
            (feedback.feedback_type == FEEDBACK_TYPE_ISSUE && feedback.issue.nil?) ||
              (feedback.feedback_type == FEEDBACK_TYPE_MERGE_REQUEST && feedback.merge_request.nil?)
          end
        end

        def create_issue_links_for(feedbacks)
          feedbacks.each do |feedback|
            ::ApplicationRecord.transaction do
              feedback, vulnerability = yield(feedback)

              if feedback.feedback_type == FEEDBACK_TYPE_ISSUE
                create_issue_link(
                  feedback: feedback,
                  vulnerability: vulnerability
                )
              else
                create_merge_request_link(
                  feedback: feedback,
                  vulnerability: vulnerability
                )
              end
            end
          end
        end

        def handle_vulnerability_present_scenario(feedbacks)
          create_issue_links_for(feedbacks) do |feedback|
            finding = feedback.finding.lock!("FOR SHARE")

            [feedback, finding.vulnerability]
          end
        end

        def handle_vulnerability_finding_present_scenario(feedbacks)
          create_issue_links_for(feedbacks) do |feedback|
            finding = feedback.finding.lock!("FOR SHARE")

            project = ::Project.find(feedback.project_id)
            author = ::User.find(feedback.author_id)

            # https://docs.gitlab.com/ee/development/database/batched_background_migrations.html#isolation
            # forbids using application code in background migrations but we have an exception for this
            # in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/97699#note_1102465241
            vulnerability = ::Vulnerabilities::CreateService.new(
              project,
              author,
              finding_id: finding.id,
              state: 'dismissed',
              skip_permission_check: true
            ).execute

            if vulnerability.errors.any?
              log_error(
                message: "Failed to create Vulnerability",
                errors: vulnerability.errors.full_messages.join("; ")
              )
              raise ActiveRecord::Rollback
            end

            [feedback, vulnerability]
          end
        end

        def handle_security_findings_only_scenario(feedbacks)
          create_issue_links_for(feedbacks) do |feedback|
            params = {
              security_finding_uuid: feedback.security_finding.uuid
            }

            project = ::Project.find(feedback.project_id)
            author = ::User.find(feedback.author_id)

            # https://docs.gitlab.com/ee/development/database/batched_background_migrations.html#isolation
            # forbids using application code in background migrations but we have an exception for this
            # in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/97699#note_1102465241
            response = ::Vulnerabilities::FindOrCreateFromSecurityFindingService.new(
              project: project,
              current_user: author,
              params: params,
              state: 'dismissed',
              skip_permission_check: true
            ).execute

            if response.error?
              log_error(message: "Failed to create Vulnerability from Security::Finding", error: response.message)
              raise ActiveRecord::Rollback
            end

            vulnerability = response.payload[:vulnerability]

            [feedback, vulnerability]
          end
        end

        def create_issue_link(feedback:, vulnerability:)
          issue_link = IssueLink.new(
            issue: feedback.issue,
            vulnerability: vulnerability
          )

          return if issue_link.save

          log_error(
            message: "Failed to create a Issue Link",
            errors: issue_link.errors.full_messages.join(", "),
            feedback_id: feedback.id,
            vulnerability_id: feedback.finding.vulnerability_id
          )
        end

        def create_merge_request_link(feedback:, vulnerability:)
          merge_request_link = MergeRequestLink.new(
            merge_request: feedback.merge_request,
            vulnerability: vulnerability
          )

          return if merge_request_link.save

          log_error(
            message: "Failed to create a Merge Request Link",
            errors: merge_request_link.errors.full_messages.join(", "),
            feedback_id: feedback.id,
            vulnerability_id: feedback.finding.vulnerability_id
          )
        end

        def log_error(message:, **rest)
          ::Gitlab::AppLogger.error(
            class: "CreateVulnerabilityLinks",
            message: message,
            **rest
          )
        end
      end
    end
  end
end
